Charlie Calvert's C++ Builder Unleashed
- 27 -
Distributed COM
Overview
The Distributed Component Object Model (DCOM) allows you to share objects easily
over a network. In this chapter, you will see how to use BCB to implement DCOM. In
particular, you will see how to create two applications that can control one another
across a network and how to create distributed database applications. In the process
of describing this technology, I will also show you how to create local OLE Automation
objects.
BCB and Windows NT 4.0 provide full support for DCOM. You can also easily add
DCOM client support to Windows 95. If you want, you can also add DCOM server support
to Windows 95, though this option has some drawbacks and limitations.
The following subjects are covered in this chapter:
- An overview of DCOM technology
- Some caveats and general comments about DCOM, including a brief overview of competing
technologies such as DSOM and CORBA
- Using TAutoObject to implement a DCOM server
- Creating a DCOM client
- Reviewing some key points about the Registry
- Working with remote datasets
- Using DCOM to access a database server on a different system without loading
any database tools on your current system
At the time of this writing, DCOM is not built into Windows 95, although it is
built into Windows NT 4.0. To add DCOM support to Windows 95, you can download Windows
95 DCOM from the Microsoft Web server. You should start looking for it in the OleDev
section: www.microsoft.com/oledev.
Alternatively, you can purchase a product called OLE Enterprise (OLEnterprise) from
Borland.
What Is DCOM?
Before I begin describing the fairly simple technical steps involved in implementing
DCOM, perhaps I should talk about this technology from a high level so that all the
key ideas will be clear to everyone. If you already understand DCOM and just want
to see how to implement it in BCB, you can skip this section.
Distributed COM is important because it allows applications to talk to one another
across a network. In particular, it allows you to share objects that reside on two
separate machines. You therefore can create an object in one application or DLL and
then call the methods of that object from an application or DLL that resides on a
different computer.
DCOM is built on top of COM, which is the technology that underlies both OLE and
ActiveX. The relationship between COM and OLE is a bit confusing, and the boundaries
separating the two technologies seem to shift at times, depending on the vagaries
of the Microsoft marketing machine. In general, I can safely say that OLE is a subset
of COM. That is, everything that is part of OLE is also part of COM, but not everything
that is part of COM is a part of OLE. Many people, however, use the words "COM"
and "OLE" virtually interchangeably, and indeed, the two technologies are
very closely bound together.
COM is simply a specification for defining an object hierarchy. In particular,
it lays out a set of rules for defining objects that can be used across applications
and languages. DCOM extends this specification to allow objects on separate machines
to talk to one another.
One of the most important aspects of this technology is that it allows you to
distribute the load of a task across several machines. For example, if you have a
complex database query to run, you can use DCOM to ask an object on a separate machine
to run it. That way, your current processor will not have to expend any clock cycles
on the task, nor will any large database- related tools be loaded into memory on
your own machine. You are therefore free to continue playing DOOM or Quake while
your server loses clock cycles and precious RAM to your background task.
You need to understand that the COM specification, as the name itself implies,
is really only a set of rules for defining an object hierarchy. These rules include
defining the names and methods of many of the key objects in the hierarchy, as well
as the specific techniques for structuring the objects themselves. (When you think
of the definition this way, you might find some value in regarding OLE as an implementation
of certain parts of the COM specification.)
The COM specification can be compared to the specification for other object hierarchies
such as VCL, OWL, or MFC. For example, all COM objects descend from a base class
called IUnknown, just as all VCL objects descend from a base class called
TObject. COM supports polymorphism and encapsulation, and it uses a series
of unique interfaces to achieve the same ends as traditional inheritance in standard
object-oriented languages. Unlike OWL or VCL, however, COM is not tied to any particular
language, nor is it bound by application boundaries.
In short, COM is an alternative to the VCL, OWL, or MFC that attempts to go these
object hierarchies one better by allowing you to use COM objects across language,
application, and now even machine boundaries. As a result, you can write a COM object
in Object Pascal, extend it in BCB, and then use it in a third language such as Visual
Basic. You can call methods of the object from inside a single application, from
one application that is calling into a DLL, or from one application that is calling
an object in a separate application. What DCOM brings to the picture is the capability
to call objects that reside in applications located on separate machines. It allows
you to "distribute" the objects across the network. In particular, this
capability allows you to divide up the load of running a major task across several
machines.
If you already understand COM, then you are ready to use DCOM without any further
work. DCOM works exactly the same way that COM works. You can, in fact, at least
theoretically convert existing COM objects into DCOM objects with no change to your
code. This system works great between two Windows NT machines, but Windows 95 requires
that you switch from Share Level to User Level access, which might crimp your style
in some cases. In particular, User Level sharing requires that an NT machine or some
other source of user access lists be available on your network.
NOTE: You can switch from Share Level
access to User Level access via the Network applet found in the Control Panel. To
then help configure your server, you can use the DComCfg.exe application
freely available from Microsoft's Web server.
If you don't want to switch to User Level access, you can choose an alternative
that will allow you to run DCOM as a client service on Windows 95 machines. In that
case, you need to add only a single new parameter to your calls into an OLE function
named CoGetClassObject. I'll explain more in the technical section of this
chapter.
NOTE: Consider these three points: n In
most cases, User Level access controls require that you have an NT machine in your
network. n NT machines don't require any special configuration to be able to act
as a DCOM server. n You cannot remotely launch an OLE server that resides on a Windows
95 machine. The process must be in memory before you can call it. This is not true
of NT machines, which can automatically launch a server if it is not already in memory.
The upshot of this point is that you can't set up a DCOM server unless you have an
NT machine on your network, and even under the best circumstances, a Windows 95 box
is crippled as a server. Given these facts, I prefer to have the NT machine act as
my DCOM server and to let the Windows 95 machines act only as clients. When connecting
the two via DCOM for the first time, sign on to both machines with the same name
and password. The simplest way to do this is to create a new user on the server with
the same name and password you use on your Win95 box. Then sign on to the NT server
with this name and password and sign on to your Windows 95 machine with the same
name and password. Once you get things working this way, then you can try connecting
with more stringent security.
The key point to grasp here is that DCOM is really nothing more than a new capability
added to the already-existing COM technology. If you have working COM objects, upgrading
them to work with DCOM is easy.
At the time of this writing (February '97), DCOM has been working on Windows NT
4.0 for over six months, but Microsoft has only just released DCOM for Windows 95.
Microsoft has stated that, in the future, COM and DCOM will be ported to other platforms
such as UNIX and the Mac.
Why Is COM Controversial?
Having, in a sense, made the case for COM and DCOM in the preceding section, I
should perhaps step back for a moment and describe some competing systems. In this
technical chapter, I'm not interested in advocating any particular system. However,
describing the current state of this technology is probably worthwhile so that you
can put this chapter in perspective.
COM is a Microsoft technology that is competing with similar technologies such
as CORBA and DSOM, which are created by other corporations or groups of corporations.
Adherents of these alternative technologies can rally numerous arguments regarding
who implemented what first and who has designed the most sophisticated technology.
Furthermore, many people have invested themselves heavily in technologies such as
OWL or MFC that can be seen as "competing," in some sense, with COM. OWL,
MFC, and the VCL don't have the same capabilities as COM, DSOM, OPENDOC, or CORBA,
but you still get a feeling that the technologies are to some degree competing for
the mind share of contemporary programmers. There is no specific reason that you
can't use COM and VCL in the same program, and indeed that is the approach I take
in this chapter.
My point here is not to advocate any particular solution, but only to make it
clear that this controversial topic tends to excite strong opinions. If you're considering
using COM in your projects, you might also want to look at CORBA and SOM. Conversely,
if you hear criticisms of COM from other members working in the industry, you might
check to see whether they are so heavily invested in some alternative technology
that they are perhaps somewhat unfairly predisposed to be critical of COM and DCOM.
DCOM, IDispatch, Marshaling, and
OLE Automation
BCB programmers can use the IDispatch COM interface to gain easy access
to the capabilities of DCOM. This interface is encapsulated inside the BCB TAutoObject
class. If you understand TAutoObject and the theory behind IDispatch
and IMarshal, you can probably skip this section and move on to the next.
OLE Automation is a technique that allows you to control one application from
inside a second application. In particular, it allows you to control an object placed
inside one application from the code of a second application.
The key to OLE Automation is a COM object called IDispatch. All OLE technologies
are based on COM, and in this particular case the functionality behind OLE Automation
is implemented by IDispatch. In short, OLE Automation is really just a marketing
term for publicizing the technology found in IDispatch. Or, more charitably,
OLE Automation is an implementation of the IDispatch specification.
IDispatch is not difficult to understand, but it can be a bit awkward
at times to implement. To help simplify the use of IDispatch, the VCL has
a class called TAutoObject that encapsulates all the functionality of IDispatch
inside an easy-to-use and highly leveraged technology.
In this chapter, I focus much of the technical content on an analysis of TAutoObject.
However, you can automate any COM object, not just IDispatch. I have chosen
to concentrate on this one technology at the exclusion of others because it provides
a simple workaround to the difficult problem of trying to marshal code and data back
and forth between two applications.
Marshaling is a COM-specific term for the technique used to transfer data or function
calls back and forth between two applications that reside in separate processes.
For example, if you have to pass a parameter to a function between two applications,
you have to be sure that it is treated properly by both applications. For example,
if you declare the parameter as an Integer in Pascal, that means you're
passing a four-byte ordinal value. How do you express that same concept in C? How
do you do it Visual Basic? The answers to these questions are expressed in COM by
a complex interface called IMarshal that is beyond the scope of this chapter.
Indeed, IMarshal is notorious for being difficult to implement.
Here is how the Microsoft documentation defines IMarshal: "`Marshaling'
is the process of packaging data into packets for transmission to a different process
or machine. `Unmarshaling' is the process of recovering that data at the receiving
end. In any given call, method arguments are marshaled and unmarshaled in one direction,
while return values are marshaled and unmarshaled in the other." This is all
good and well. Unfortunately, as I stated earlier, the IMarshal interface
is very hard to implement.
If you're using a standard COM object, you don't have to implement IMarshal
because these interfaces will be marshaled for you automatically by the system. In
other words, if you're implementing an instance of IDispatch, IUnknown,
IClassFactory, IOleContainer, or any other predefined COM class,
you don't have to worry about marshaling. Microsoft will take care of this job for
you. However, if you're creating a custom object of your own, you need to implement
IMarshal or come up with some alternative scheme.
Because of the complexity of IMarshal, C programmers also generally choose
not to attempt an implementation of IMarshal. Instead, they rely on an intermediate
language called Interface Definition Language (IDL) that can be compiled into source
code by a Microsoft-created program called MIDL.EXE. The IDL is a special
language meant to allow people to define interfaces in a neutral language that can
be compiled into source that can be used by multiple languages such as Pascal, C,
and Visual Basic.
In other words, you can theoretically write your COM object in C or Pascal, use
IDL to define its interface, and then use MIDL to turn that interface into a set
of files that can be used by any language. In other words, MIDL automatically takes
care of the IMarshal business for you as long as you first describe your
interface in IDL.
This approach is quite reasonable, but I will not treat it in this current book,
in part because BCB does not ship with MIDL. It is, however, available from the Microsoft
SDKs and may be freely available via their Web site. You should also note that Delphi
3.0 includes tools that automate this process without your having to learn IDL or
work with MIDL.
For now, however, I will back away from both IMarshal (because it is
so complex) and MIDL (because it doesn't ship with BCB). This situation would appear
to leave no good way to handle DCOM, were it not for the power of IDispatch
and TAutoObject. IDispatch is a COM interface designed to make
controlling one application from inside a second application easy. In implementing
this code, Microsoft provided an alternative means for solving the whole problem
of marshaling data between applications. In TAutoObject, the VCL provides
a very simple means of using IDispatch.
Thinking About IDispatch
Here is how Microsoft defines IDispatch: "IDispatch is
a COM interface that is designed in such a way that it can call virtually any other
COM interface." In other words, if you put a COM object in an application, you
can call its methods from a second application by using IDispatch. This
way, OLE Automation allows you to control one application from inside a second application.
(In particular, IDispatch was created to help make COM programming easier
from inside the limited confines of a Visual Basic application.)
To understand why IDispatch works, you need to remember that marshaling
is taken care of for you automatically as long as you're using an existing COM interface.
In other words, you don't have to implement marshaling for IDispatch because
it is a standard COM object, not a custom object designed by yourself or someone
on your team. IDispatch exists to allow you to call the methods of any legal
COM object. In other words, it is designed to solve the whole problem of marshaling
data. As such, it is the perfect solution for BCB programmers who want to use DCOM
without engaging in too much manual labor.
Before closing this section, I should emphasize that you don't have to use IDispatch.
If you prefer, you can use other predefined COM objects, or you can implement IMarshal,
or you can attempt to use MIDL. BCB has none of the limitations found in languages
like Visual Basic, so you don't need to be confined to using IDispatch unless
you find its relative simplicity appealing.
Using TAutoObject to Implement
a DCOM Server
Now you're ready to move away from theoretical issues and to concentrate instead
on technical matters. Ironically, the theory behind this technology is much harder
to understand than the technology itself. In short, in this section and the next,
I outline a simple technique for using DCOM that can be used by any intermediate-level
BCB programmer.
You learned in the preceding sections that TAutoObject is BCB's wrapper
around IDispatch. IDispatch is the COM object that makes OLE Automation
possible. In this section, I show you how to implement OLE Automation that works
not only between two applications, but also between two applications that reside
on separate machines.
If you go to the File menu in BCB and choose New, you can pop up the Object Repository.
On the first page of the Object Repository is an icon you can select if you want
to create an Automation object. After selecting the Automation Object icon, you are
presented with a dialog, as shown in Figure 27.1.
FIGURE
27.1. Selecting the Automation object
from the Object Repository.
You can fill in the fields of this dialog as you like, or you can put in the following
default values:
Class Name: TMyDCOM
OLE Class Name: MyProj.MyDCom
Description: My DCOM Object
Instancing: Multiple Instance
When you're done, BCB spits out two pages of code as shown in Listing 27.1 and
Listing 27.2. As you can see, I have, for the sake of fidelity to the compiler's
output and contrary to habit, left in the mystifying series of dashes inserted by
the compiler.
Listing 27.1. The header file produced
by the BCB OLE Automation Wizard.
//--------------------------------------------------------------------------
#ifndef Unit1H
#define Unit1H
//--------------------------------------------------------------------------
#include <vcl\OleAuto.hpp>
#include <vcl\Classes.hpp>
//--------------------------------------------------------------------------
class TMyDCom : public TAutoObject
{
private:
public:
__fastcall TMyDCom();
__automated:
};
//--------------------------------------------------------------------------
#endif
Listing 27.2. The main source
file produced by the OLE Automation Wizard.
//--------------------------------------------------------------------------
#include <vcl\vcl.h>
#pragma hdrstop
#include "Unit1.h"
//--------------------------------------------------------------------------
__fastcall TMyDCom::TMyDCom()
: TAutoObject()
{
}
//--------------------------------------------------------------------------
void __fastcall RegisterTMyDCom()
{
TAutoClassInfo AutoClassInfo;
AutoClassInfo.AutoClass = __classid(TMyDCom);
AutoClassInfo.ProgID = "MyProj.MyDCom";
AutoClassInfo.ClassID = "{FCB9F540-87FF-11D0-BCD7-0080C80CF1D2}";
AutoClassInfo.Description = "My DCOM Object";
AutoClassInfo.Instancing = acMultiInstance;
Automation->RegisterClass(AutoClassInfo);
}
//--------------------------------------------------------------------------
#pragma startup RegisterTMyDCom
//---------------------------------------------------------------------------
The RegisterTMyDCOM procedure is used to register your object with the
system--that is, to list it in the Registry. The details of this process are described
in the section called "Registration Issues." For now, you need only take
note of the ClassID assigned to your object because you will need this ID
when you try to call the object from another machine, as described in the next section.
The act of registering the object is not something you necessarily have to understand
because it will occur automatically whenever you run the client application of which
TMyDCOM is a part.
NOTE: The object will be registered repeatedly,
whenever you run the program, which ensures that you will find it easy to register
the object, while simultaneously requiring very little overhead in terms of system
resources. If you move the application to a new location, you can register this change
with the system by running it once. This capability guarantees that the old items
associated with your CLSID will be erased, and new items will be filled in their
place. Registering a class ID multiple times does not mean that you will end up with
multiple items in the Registry because each registration of a CLSID will overwrite
the previous registration. All OLE servers worth their name provide this service.
For example, Word and Excel update the Registry each time they are run.
Besides the registration procedure, the other key part of the code generated by
the Automation expert is the class definition found at the top of the header:
class TMyDCom : public TAutoObject
{
private:
public:
__fastcall TMyDCom();
__automated:
};
This code has two sections, one called private and the other called automated.
In the __automated section, you can declare methods or properties that you
want to call across program or machine boundaries. In other words, any methods or
properties that you declare in this space will automatically be marshaled for you
by the underlying IDispatch object encapsulated by TAutoObject.
Consider the following code fragments:
class TSimpleDCOM : public TAutoObject
{
private:
public:
virtual __fastcall TSimpleDCOM();
__automated:
AnsiString __fastcall GetName();
int __fastcall Square(int A);
};
AnsiString __fastcall TSimpleDCOM::GetName()
{
return "SimpleDCOM";
}
int __fastcall TSimpleDCOM::Square(int A)
{
return A * A;
}
This object has two methods: one that states the name of the object and one that
can square an integer. These two methods are declared in the automated section
of the object, so they can be accessed from another program via another program.
The TSimpleDCOM object exports two methods that IDispatch will
automatically marshal for you across application or machine boundaries. You can go
on adding methods to this object as you like. Any data that you want to add to the
object should go in the private section, and any methods or properties that
you don't want to export should also go in the private section. All methods
that you want to call from inside another application should go in the automated
section. You should declare these exported methods as __fastcall.
Some limits to the marshaling will be done for you by IDispatch. In particular,
the following types are legal to use in the declarations for the methods or properties
in the automated section:
int,
float,
double,
Currency,
TDateTime,
AnsiString,
WordBool
Short
String
unsigned short
Variant
The following types are illegal to use in the declarations for the methods or
properties in the automated section:
arrays
char *
void *
structs
For additional information, see the "Automating properties and methods"
section in the online help for the VCL.
The apparent limitations created by the lack of support from IDispatch
for custom types can be considerably mitigated by an intelligent use of variant arrays.
These structures can be so helpful that I have added a section later in this chapter
called "Using Variant Arrays to Pass Data" to describe their use.
The complete source for a simple DCOM server is shown in Listing 27.3 through
Listing 27.6. Notice that OleAuto is included in this project. This unit
is essential to OLE Automation programming with the VCL.
Listing 27.3. The heading for the
SimpleObject file from the EasyDCOM project.
///////////////////////////////////////
// SimpleObject.h
// EasyDCOM
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef SimpleObjectH
#define SimpleObjectH
#include <vcl\oleauto.hpp>
#include <vcl\Classes.hpp>
class TSimpleDCOM : public TAutoObject
{
private:
public:
virtual __fastcall TSimpleDCOM();
__automated:
AnsiString __fastcall GetName();
int __fastcall Square(int A);
};
#endif
Listing 27.4. The main source
file of an OLE Automation object.
///////////////////////////////////////
// SimpleObject.cpp
// EasyDCOM
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#undef RegisterClass
#include "SimpleObject.h"
int Initialization();
static int Initializer = Initialization();
__fastcall TSimpleDCOM::TSimpleDCOM()
: TAutoObject()
{
}
AnsiString __fastcall TSimpleDCOM::GetName()
{
return "SimpleDCOM";
}
int __fastcall TSimpleDCOM::Square(int A)
{
return A * A;
}
void __fastcall RegisterTSimpleDCOM()
{
TAutoClassInfo AutoClassInfo;
AutoClassInfo.AutoClass = __classid(TSimpleDCOM);
AutoClassInfo.ProgID = "EasyDCOM.SimpleDCOM";
AutoClassInfo.ClassID = "{E2674A60-2DF2-11D0-92C5-000000000000}";
AutoClassInfo.Description = "Easiest possible DCOM program";
AutoClassInfo.Instancing = acMultiInstance;
Automation->RegisterClass(AutoClassInfo);
}
int Initialization()
{
RegisterTSimpleDCOM();
return 0;
}
Listing 27.5. The header for
the main source file for the EasyDCOM OLE server.
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
class TForm1 : public TForm
{
__published:
TLabel *Label1;
private:
public:
virtual __fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 27.6. The main source
file for the EasyDCOM OLE server.
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
This program is meant to be run from a client. As such, it has no controls on
it and no public interface other than the OLE object itself. I do, however, give
the main form a distinctive look, as you can see in Figure 27.2.
FIGURE
27.2. The main form for the EasyDCOM program.
Of course, there is no reason that a single program could not simultaneously have
an OLE server interface and a set of standard controls. For example, Word and Excel
are both OLE servers, and standard applications run through a set of menus and other
controls. In fact, the same application can work as a server, a standard application,
and as a client.
Note that, by using two different approaches, you can ensure that the application
is registered each time it is run. One technique involves including an initialization
procedure:
int Initialization()
{
RegisterTSimpleDCOM();
return 0;
}
The second technique, shown earlier in the chapter, involves a pragma:
#pragma startup RegisterTSimpleDCom
Both technologies achieve the same effect. As a rule, you don't have to think
about this part of the process because the code will be inserted automatically by
the Automation Wizard. Needless to say, nothing is magic about the Automation expert,
and you can simply create the code yourself by typing it in. In that case, you are
free to use either technique, though the pragma is probably easier to write.
That's all I'm going to say for now about creating the server side of a BCB DCOM
project. Remember that this code will not work unless you first register the TSimpleDCOM
object with the system by running the server once. After you run the server the first
time, you never have to run it again, as it will be called automatically by the client
program described in the next section. Let me repeat that the whole point of this
exercise is that the client program can be located on a separate machine.
Creating the DCOM Client
The GetDCOM program found on the CD that accompanies this book will call the functions
in the server program described in the preceding section. In particular, GetDCOM
can automatically launch the server program and then call its GetName and
Square functions.
NOTE: When I say that GetDCOM can automatically
launch the server, I'm assuming that the server is either on the current system (in
which case, it is launched via COM) or on an NT machine (in which case, it is launched
via DCOM). DCOM cannot launch an application residing on a remote Windows 95 box.
You can run this application in two different modes. You can run it as a client
to a local Automation server or as a client to a remote Automation server. If you
look at the main form for the program, shown in Figure 27.3, you can see that it
has three buttons: one for launching the server remotely, one for launching it locally,
and a third that will be used to call a simple function on the server.
FIGURE
27.3. The main form for the GetDCOM application.
The source for the GetDCOM program is shown in Listing 27.7 and Listing 27.8.
This program uses a routine called CreateRemoteObject that is declared in
the CodeBox unit found in the Utils subdirectory on the CD that
accompanies this book. You need to add the CodeBox unit to your project;
otherwise, it will not compile. I do not include the entire CodeBox unit
in this chapter, but it is available on the CD, and I do include the CreateRemoteObject
function in its entirety later in this chapter. Notice also that this project includes
the OleAuto unit to call CreateOleObject to retrieve a local instance
of IDispatch.
When using this program, please note that I have hard coded the IP address of
my server into the source. You will need to change this so that it works with your
server. When making the connection between a Windows 95 and Windows NT machine, you
should start by calling from the Windows 95 machine to the Windows NT machine; that
is, put the client on the Windows 95 machine. You should also start by signing on
to both machines with the same name and password. That way you don't have to worry
about security issues on the server while you are first getting the technology up
and running. Also, give yourself all possible rights on the server. Make yourself
an administrator.
Listing 27.7. The header for the
GetDCOM OLE client application.
///////////////////////////////////////
// Main.h
// Project: GetDCOM
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\Buttons.hpp>
class TForm1 : public TForm
{
__published:
TBitBtn *GetLocalObjectBtn;
TBitBtn *GetRemoteObjectBtn;
TEdit *Edit1;
TBitBtn *SquareBtn;
void __fastcall GetLocalObjectBtnClick(TObject *Sender);
void __fastcall GetRemoteObjectBtnClick(TObject *Sender);
void __fastcall SquareBtnClick(TObject *Sender);
void __fastcall FormDestroy(TObject *Sender);
private:
Variant V;
public:
virtual __fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 27.8. The main source
file for the GetDCOM application.
///////////////////////////////////////
// Main.cpp
// Project: GetDCOM
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <vcl\OleAuto.hpp>
#include <vcl\ole2.hpp>
#include <initguid.h>
#pragma hdrstop
#include "Main.h"
#include "codebox.h"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
CoInitialize(NULL);
}
void __fastcall TForm1::GetLocalObjectBtnClick(TObject *Sender)
{
V = CreateOleObject("EasyDCOM.SimpleDCOM");
ShowMessage(V.OleFunction("GetName"));
}
DEFINE_GUID(ClassID, 0xE2674A60, 0x2DF2, 0x11D0, 0x92,0xC5,
0x00,0x00,0x00,0x00,0x00,0x00);
void __fastcall TForm1::GetRemoteObjectBtnClick(TObject *Sender)
{
Screen->Cursor = crHourGlass;
if (CreateRemoteObject(ClassID, "143.186.149.228", V))
{
ShowMessage(V.OleFunction("GetName"));
}
else
{
ShowMessage("Failed");
}
Screen->Cursor = crDefault;
}
void __fastcall TForm1::SquareBtnClick(TObject *Sender)
{
try
{
ShowMessage(V.OleFunction("Square", Edit1->Text));
}
catch(Exception &E)
{
ShowMessage(E.Message);
}
}
void __fastcall TForm1::FormDestroy(TObject *Sender)
{
CoUninitialize();
}
The code declares the CLSID created by the BCB Automation expert in the preceding
section of this chapter:
DEFINE_GUID(ClassID, 0xE2674A60, 0x2DF2, 0x11D0, 0x92,0xC5,
0x00,0x00,0x00,0x00,0x00,0x00);
This is the CLSID associated with the server half of this DCOM project, and you
need to include it here in the client program. Additional information about CLSIDs
and the registration process will be presented later in this chapter.
NOTE: You need to include the standard
Windows API initguids.h file in projects that use GUIDs.
The actual call to automate the object is nearly identical to the call you would
make if you wanted to automate an object on your local machine. The only difference
is that you call CreateRemoteOleObject rather than CreateOleObject.
Here is the way to call the object locally:
void __fastcall TForm1::GetLocalObjectBtnClick(TObject *Sender)
{
V = CreateOleObject("EasyDCOM.SimpleDCOM");
ShowMessage(V.OleFunction("GetName"));
}
This code assumes that the variable V, of type Variant is a
field of TForm1. The code calls a built-in function of the VCL called CreateOleObject.
This function takes the ProgID of an object, looks up its CLSID in the Registry,
finds the place on the hard drive where the program that owns the object is located,
launches the program, and retrieves the object in a Variant.
The code then uses the OleFunction method of the Variant object
to call one of the methods of the OLE server. OleFunction takes one or more
parameters, specifying the name of the function you want to call and any parameters
you want to pass to it. BCB does not have support for named parameters.
Later in the program, you can call the Square method of the Automation
server:
void __fastcall TForm1::SquareBtnClick(TObject *Sender)
{
try
{
ShowMessage(V.OleFunction("Square", Edit1->Text));
}
catch(Exception &E)
{
ShowMessage(E.Message);
}
}
Again, I use the OleFunction method to make the call to the Square
method via OleAutomation. As you can see, I wrap the function call in a
try..except block because chances are good that the user might click the
Square button before initializing the object with a call to CreateOleObject
or CreateRemoteOleObject. Notice that this time OleFunction takes
two parameters, one stating the name of the OLE server method to be called and the
second specifying a parameter to be passed to that method.
Here is the method of GetDCOM that I use to summon the remote server:
void __fastcall TForm1::GetRemoteObjectBtnClick(TObject *Sender)
{
Screen->Cursor = crHourGlass;
if (CreateRemoteObject(ClassID, "143.186.149.228", V))
{
ShowMessage(V.OleFunction("GetName"));
}
else
{
ShowMessage("Failed");
}
Screen->Cursor = crDefault;
}
This function is no different in substance from the GetDCOM routine that
retrieves the local object. The only difference is that I call CreateRemoteObject
rather than CreateOleObject. Remember that you can call CreateOleObject
to retrieve remote objects if you are on an NT machine or if you have set the Windows
95 server machine into User Access mode via the Network applet in the Control Panel.
NOTE: Let me just reiterate that you need
to pass in the IP address, or server name, of the machine on which your server is
located. Here I type in the IP address of my NT server: 143.186.149.228.
You replace this number with the name or number of your server. If you're confused
by the topic of IP addresses, you might be able to glean some information from the
discussion of TCP/IP in Chapter 8, "Database Basics and Database Tools."
CreateRemoteObject is a custom function I have written; it looks like
this:
BOOL CreateRemoteObject(GUID ClassID, char *Server, Variant &V)
{
Ole2::IClassFactory *ClassFactory;
Ole2::IUnknown *Unknown1;
COSERVERINFO Info;
OLECHAR Dest[MAX_PATH];
int i = MultiByteToWideChar(CP_ACP, 0, Server, -1, Dest, MAX_PATH);
if (i <= 0)
return FALSE;
ClassFactory = NULL;
Info.dwReserved1 = 0;
Info.pAuthInfo = NULL;
Info.dwReserved2 = 0;
Info.pwszName = Dest;
HRESULT hr = CoGetClassObject(ClassID, CLSCTX_REMOTE_SERVER, &Info,
Ole2::IID_IClassFactory, (void **)&ClassFactory);
OleCheck(hr);
if (ClassFactory != NULL)
{
hr = ClassFactory->CreateInstance(NULL,
Ole2::IID_IUnknown, (void **)&Unknown1);
OleCheck(hr);
V = VarFromInterface(Unknown1);
ClassFactory->Release();
if (VarType(V) != varNull)
return True;
else
return False;
}
return FALSE;
}
This routine is declared in the CodeBox unit found in the Utils
subdirectory on the CD that accompanies this book. As I stated earlier, you need
to add the CodeBox unit to your project; otherwise, it will not compile.
Alternatively, you can simply copy this routine into your project. However, keeping
it in a separate unit makes sense because you might want to call it from multiple
applications.
Whether you understand this routine is not really important. You can just plug
it into your applications the same way you do CreateOleObject. However,
I will talk about it briefly for those who are interested.
The CreateRemoteObject routine takes three parameters. The first contains
the ID of the object you want to obtain, and the second contains the name of the
server where the object resides. The last parameter contains a variant that will
hold the instance of IDispatch retrieved from the system. (Sometimes you
might have to use the IP address itself rather than the name of the server.) CreateRemoteObject
returns a variant that "contains" a copy of the object that you want to
call. You can use this variant to call all the methods in the automated
section of your object.
Variants are special BCB types that can contain a wide variety of data types,
including OLE objects. I discussed variants at some length in Chapter 3, "C++Builder
and the VCL."
When you call the methods of an OLE object off a variant, no runtime checking
for the calls occurs. BCB just assumes you know what you're doing, and if the call
fails, you won't know until runtime. This problem is addressed in Delphi 3, and so
I assume it will be addressed in future releases of C++Builder.
The key call in CreateRemoteOleObject is to CoGetClassObject:
HRESULT hr = CoGetClassObject(ClassID, CLSCTX_REMOTE_SERVER, &Info,
Ole2::IID_IClassFactory, (void **)&ClassFactory);
OleCheck(hr);
This routine has long been a part of COM, but it has been altered slightly to
support DCOM. Here is how the routine is currently declared in ObjBase.h:
WINOLEAPI CoGetClassObject(
REFCLSID rclsid, // The ID of the object you want
DWORD dwClsContext, // In process, local or remote server?
LPVOID pvReserved, // Previously reserved, now used for CoServerInfo
REFIID riid, // Usually IID_IClassFactory
LPVOID FAR* ppv); // Where the class factory is returned
The function returns an HRESULT variable containing information on the
outcome of the call. If HRESULT is set to zero, then the call succeeded.
Most other values represent an error in the form of a number. You can retrieve a
human-readable string by passing that number to a VCL function called OleCheck.
The third parameter to CoGetClassObject, previously reserved, is now
the place where you pass in the name of the server you want to access. The server
is usually designated with either a string or a literal IP address, such as 143.186.149.111.
You would pass in the IP address in the form of a string. That is, don't try to pass
a number; just put the IP address in quotation marks and pass it in as a string.
Here is the new declaration for CoGetClassObject, as found in the MSDN:
STDAPI CoGetClassObject(
REFCLSID rclsid, //CLSID associated with the class object
DWORD dwClsContext, //Context for running executable code
COSERVERINFO * pServerInfo, // Machine on which object is to be instantiated
REFIID riid, //Reference to the identifier of the interface
LPVOID * ppv //Indirect pointer to the interface
);
In particular, here is the record you pass in for the third parameter:
typedef struct _COSERVERINFO {
DWORD dwSize; // must be set to sizeof(COSERVERINFO)
OLECHAR* pszName; // machine name
} COSERVERINFO;
The first field of this record is just a version check field that should contain
the size of the TCoServerInfo record. The second parameter contains a Unicode
string that has the name of the server or its IP address embedded in it. Use the
MultiByteToWideChar Windows API function to convert a standard BCB string
into a Unicode string:
OLECHAR Dest[MAX_PATH];
int i = MultiByteToWideChar(CP_ACP, 0, Server, -1, Dest, MAX_PATH);
if (i <= 0)
return FALSE;
The call to CoGetClassObject retrieves a ClassFactory. After
you have the ClassFactory back from the server, you can use it to retrieve
an instance of the object you want to call. What you retrieve back, of course, is
an instance of IDispatch. You can convert this instance into a variant by
calling the BCB routine VarFromInterface, which is found in the OleAuto
unit that ships with BCB.
If you want, you can simplify this call by using CoCreateInstanceEx.
CoCreateInstanceEx is superior to CoGetClassObject because it retrieves
the object you want with only one call instead of having to first get the ClassFactory
and then call CreateInstance on the ClassFactory. In short, CoCreateInstanceEx
executes faster than CoGetClassObject. (Remember, all calls between objects
on separate machines are going to have a considerable overhead associated with them!)
Another advantage of CoCreateInstanceEx is that it takes a MultiQI
structure that can contain a list of multiple objects to retrieve. That way, you
can retrieve multiple objects through a single call. Again, using this method will
save considerable time.
Before I close this section, let me review the key points covered so far:
- DCOM allows you to call objects located in one application or DLL on one machine
from inside a separate application on a separate machine.
- You can use TAutoObject to create the server side of your application.
- You can use the custom CreateRemoteObject to call the server from a
client program located on a second server.
Registration Issues
Before going further, I want to mention a few issues about CLSIDs and the Registry.
If you already understand the Registry, you can skip this section. I covered some
aspects of the Registry in Chapter 13, "Flat-File, Real-World Databases."
However, I will go over this material again here from the perspective of an OLE application.
The Registry is a place where information can be stored. It's a database.
CLSIDs are statistically unique numbers that can be used by the operating system
to reference an OLE object. CLSIDs are stored in the Registry.
In this case, visiting the actual perpetrator in its native habitat is probably
best. In the example explained here, I'm assuming that you have a copy of Word loaded
on your system.
To get started, use the Run menu on the Windows taskbar to launch the RegEdit
program that ships with Windows NT. Just type RegEdit and click OK. Search
through the HKEY_CLASSES_ROOT for the Word.Basic entry, as shown
in Figure 27.4. When you find it, you can see that it's associated with the following
CLSID:
{000209FE-0000-0000-C000-000000000046}
This unique class ID is inserted into the Registry of all machines that contain
a valid, and properly installed, copy of Word for Windows. The only application that
uses this ID is Word for Windows. It belongs uniquely to that application.
Now go further up HKEY_CLASSES_ROOT and look for the CLSID branch. Open
it and search for the CLSID shown above. When you find it, you can see two entries
associated with it: one is called LocalServer, or LocalServer32,
and the other is called ProgID. The ProgID is set to word.basic.
The LocalServer entry looks something like this:
C:\WINWORD\WINWORD.EXE /Automation
FIGURE
27.4. If you run the Windows program Regedit.exe,
then you can see the registration database entry for Word.Basic under HKEY_CLASSES_ROOT.
If you look at this command, you can begin to grasp how Windows can translate
the CLSID passed to CoGetClassObject into the name of an executable. In
particular, Windows looks up the CLSID in the Registry and then uses the LocalServer32
entry to find the directory and name of the executable or DLL you want to launch.
Having these kinds of entries in the registration database does not mean that
the applications in question are necessarily Automation servers. For example, many
applications with LocalServer and ProgID entries are not Automation
servers. However, all Automation servers do have these two entries. Note, further,
that this is a reference to the Automation server in Word, not a reference to Word
as a generic application. It references an Automation object inside Word, not Word
itself. (The Automation object is an instance of IDispatch. It was not created
with TAutoObject, but it has all the same attributes.)
The same basic scenario outlined here takes place when you call CoGetClassObject
and specify the CLSID of an object on another machine. In particular, Windows contacts
the specified machine, asks it to look up the CLSID in the Registry, and then marshals
information back and forth between the two machines.
CLSIDs are said to be statistically unique. You can create a new CLSID by calling
CoCreateGuid. The following code shows one way to make this call:
CoInitialize(NULL);
CoCreateGuid(GUID);
// eventually you should call CoUninitialize;
The code shown here begins by calling CoInitialize, which is usually
unnecessary in BCB because the OLE2 unit will call this function automatically
when your program is launched; that is, it will do so if you include OLE2
in the uses clause of one of your units.
CoCreateGuid is the call that retrieves the new CLSID from the system.
This ID is guaranteed to be unique as long as you have a network card on your system.
Each network card has a unique number on it, and this card number is combined with
the date and time and other random bits of information to create a unique number
that could only be generated on a machine with your network card at a particular
date and time. Rumors that the phase of the moon and current age of Bill Gates's
children are also factored in are probably not true. At any rate, the result is a
number that is guaranteed to be statistically unique, within the tolerance levels
for your definition of that word given your faith in mathematicians in general and
Microsoft-based mathematicians in particular.
The StringFromCLSID routine converts a CLSID into a string. The ParseGuid
routine is a custom function I wrote to convert a string of type
{FC41CC90-C01D-11CF-8CCD-0080C80CF1D2}
into a record of type GUID that can be used in a BCB application as defined
in Wtypes.h:
typedef struct _GUID
{
DWORD Data1;
WORD Data2;
WORD Data3;
BYTE Data4[ 8 ];
} GUID;
That's all I want to say about the Registry for now. This subject can appear a
bit tricky at first, but ultimately it is not complicated.
Using Variant Arrays to Pass Data
BCB enables you to create variant arrays, which are the VCL version of the safe
arrays used in OLE Automation. You can use variant arrays to pass large chunks of
data back and forth between COM objects. For example, you can pass a bitmap, AVI
file, or text file between two applications using variant arrays. In short, this
type can help you avoid the shortcomings created by the limited types supported by
IDispatch.
Variant arrays (and safe arrays) are costly in terms of memory and CPU cycles,
so you normally would not use them except in automation or DCOM code, or in special
cases in which they provide obvious benefits over standard arrays. For example, the
database code makes some use of variant arrays.
The Variant class type, found in SysDefs.h and covered in Chapter
3 has constructors for creating variant arrays:
// constructor for array of variants of type varType
__fastcall Variant(const int* bounds, const int boundsSize,
Word varType);
// constructor for one-dimensional array of type Variant
__fastcall Variant(const Variant* values, const int valuesSize);
If you know the type of the elements to be used in an array, you can set the VarType
parameter to that type. For example, if you know you're going to be working with
integers, you can write the following:
Variant MyVariant(OPENARRAY(int, (0, 5)), varInteger);
You cannot use varString in the last parameter; instead, use varOleStr.
Remember that an array of Variant takes up 16 bytes for each member of the
array, and other types might take up less space.
Arrays of Variant can be resized with the VarArrayRedim function:
extern void __fastcall VarArrayRedim(Variant &A, int HighBound);
The variable to be resized is passed in the first parameter, and the number of
elements to be contained in the resized array is held in the second parameter.
You declare a two-dimensional array like this:
Variant MyVariant(OPENARRAY(int, (0, 5, 0, 5)), varInteger);
This array has two dimensions, each with six elements. To access a member of this
array, you write code that looks like the following:
for (i = 0; i < 6; i++)
for (j = 0; j < 6; j++)
MyVariant.PutElement(i * j, i, j);
for (i = 0; i < 6; i++)
{
for (j = 0; j < 6; j++)
{
S = S + " " + MyVariant.GetElement(i, j);
}
S = S + `\r';
}
The following code fragment shows how to use a one-dimensional array and how to
query an array to find out about its composition:
AnsiString TForm1::GetInfo(Variant &V)
{
int Count, HighBound, LowBound, i;
AnsiString S;
Count = VarArrayDimCount(V);
S = AnsiString("\nDimension Count: ") + IntToStr(Count) + `\n';
for (i = 1; i <= Count; i++)
{
HighBound = VarArrayHighBound(V, i);
LowBound = VarArrayLowBound(V, i);
S = S + "LowBound: " + IntToStr(LowBound) + `\n';
S = S + "HighBound: " + IntToStr(HighBound) + `\n';
}
return S + `\n';
}
void __fastcall TForm1::bOneDimClick(TObject *Sender)
{
AnsiString S;
int i;
S = "";
Variant MyVariant(OPENARRAY(int, (0, 5)), varInteger);
for (i = 0; i <= 5; i++)
MyVariant.PutElement(i * 2, i);
for (i = 0; i <= 5; i++)
S = S + " " + MyVariant.GetElement(i);
S = GetInfo(MyVariant) + S;
ShowMessage(S);
}
The GetInfo method demonstrates how to work with a variant array passed
as a parameter. Notice that you don't have to do anything special to access a variant
as an array. The type travels with the variable.
If you try to pass a variant with a VType of varInteger to this
function, BCB raises an exception when you try to treat the variant as an array.
In short, the variant must have a VType of VarArray; otherwise,
the call to GetInfo will fail. You can use the VarType function
to check the current setting for the VType of a variant, or you can call
VarIsArray, which returns a Boolean value.
You can use the VarArrayHighBound, VarArrayLowBound, and VarArrayDimCount
functions to find out about the number of dimensions in your array and about the
bounds of each dimension. The following GetInfo function creates a string
showing the number of dimensions in a variant array, as well as the high and low
values for each dimension:
AnsiString TForm1::GetInfo(Variant &V)
{
int Count, HighBound, LowBound, i;
AnsiString S;
Count = VarArrayDimCount(V);
S = AnsiString("\nDimension Count: ") + IntToStr(Count) + `\n';
for (i = 1; i <= Count; i++)
{
HighBound = VarArrayHighBound(V, i);
LowBound = VarArrayLowBound(V, i);
S = S + "LowBound: " + IntToStr(LowBound) + `\n';
S = S + "HighBound: " + IntToStr(HighBound) + `\n';
}
return S + `\n';
}
This routine starts by getting the number of dimensions in the array. It then
iterates through each dimension, retrieving its high and low values. If you create
an array with the call
Variant MyVariant(OPENARRAY(int, (0, 5, 1, 3)), varInteger);
the GetInfo function produces the following output if passed MyVariant:
Dimension Count: 2
HighBound: 5
LowBound: 0
HighBound: 3
LowBound: 1
GetInfo raises an exception if you pass in a variant that causes VarIsArray
to return False.
A certain amount of overhead is involved in working with variant arrays. If you
want to process the arrays quickly, you can use two functions called VarArrayLock
and VarArrayUnlock. The first of these routines returns a pointer to the
data stored in an array. In particular, VarArrayLock takes a variant array
and returns a standard Pascal array. For it to work, the array must be explicitly
declared with one of the standard types listed earlier in the chapter. The type used
in the variant array and the type used in the Pascal array must be identical.
Here is an example of using VarArrayLock and VarArrayUnlock:
Variant GetArrayData()
{
int i, j;
Variant V(OPENARRAY(int, (1, Max, 1, Max)), varInteger);
for (i = 1; i < Max; i++)
for (j = 1; j < Max; j++)
V.PutElement(i * j, j, i);
return V;
}
void __fastcall TForm1::LockedArray1Click(TObject *Sender)
{
int Data[Max][Max];
int i, j;
Variant V;
V = GetArrayData();
void *P = VarArrayLock(V);
memcpy(Data, P, sizeof(Data));
for (i = VarArrayLowBound(V, 1); i < VarArrayHighBound(V, 1); i++)
for (j = VarArrayLowBound(V, 2); j < VarArrayHighBound(V, 2); j++)
Grid->Cells[i-1][j-1] = Data[i-1][j-1];
VarArrayUnlock(V);
}
Notice that this code first locks down the array and then accesses it as a pointer
to a standard array. Finally, it releases the array when the operation is finished.
You must remember to call VarArrayUnlock when you're finished working with
the data from the array:
for (i = VarArrayLowBound(V, 1); i < VarArrayHighBound(V, 1); i++)
for (j = VarArrayLowBound(V, 2); j < VarArrayHighBound(V, 2); j++)
Grid->Cells[i-1][j-1] = Data[i-1][j-1];
VarArrayUnlock(V);
Remember that the point of using VarArrayLock and VarArrayUnlock
is that they speed access to the array. The actual code you write is more complex
and verbose, but the performance is faster.
If you don't want to lock down an array, you can still access the data. You have
to do so by brute-force means, however, and can't use vast pointer-manipulation routines
such as memcpy. The following NormalArray1Click method shows how
to proceed if you don't lock down the data:
void __fastcall TForm1::NormalArray1Click(TObject *Sender)
{
int i, j;
Variant V = GetArrayData();
for (i = 1; i < VarArrayHighBound(V, 1); i++)
for (j = 1; j < VarArrayHighBound(V, 2); j++)
Grid->Cells[i-1][j-1] = V.GetElement(i, j);
}
One of the most useful reasons for using a variant array is to transfer binary
data to and from a server. If you have a binary file, say a WAV file or an AVI file,
you can pass it back and forth between your program and an OLE server using variant
arrays. Such a situation would present an ideal time for using VarArrayLock
and VarArrayUnlock. You would, of course, use VarByte as the second
parameter to VarArrayCreate when you're creating the array. That is, you
would be working with an array of Byte and accessing it directly by locking
down the array before moving data into and out of the structure. Such arrays are
not subject to translation while being marshaled across boundaries.
The next program in this chapter shows how to pass data back and forth between
programs using this technique. Listing 27.9 and Listing 27.10 contain a single sample
program that encapsulates most of the ideas that you have seen in this section on
variant arrays. The program from which this code is excerpted is called VarArray,
and you can find it in the Chap27 directory on the disk. Some screen shots
from the program are shown in Figure 27.5 and Fig- ure 27.6.
FIGURE
27.5. Using the VarArray program to view
information about a two-dimensional array.
FIGURE
27.6. Viewing a two-dimensional array
that is locked down to get fast access to its data.
Listing 27.9. The header for
the VarArray program. VarArray is designed to show how to use variant arrays.
///////////////////////////////////////
// Main.cpp
// Project: VarArray
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include "Grids.hpp"
#include <vcl\Menus.hpp>
class TForm1 : public TForm
{
__published:
TStringGrid *Grid;
TMainMenu *MainMenu1;
TMenuItem *Options1;
TMenuItem *CreateOneDimensionalArray1;
TMenuItem *CreateTwoDimensionalArray1;
TMenuItem *NormalArray1;
TMenuItem *LockedArray1;
void __fastcall bOneDimClick(TObject *Sender);
void __fastcall bTwoDimClick(TObject *Sender);
void __fastcall NormalArray1Click(TObject *Sender);
void __fastcall LockedArray1Click(TObject *Sender);
private:
AnsiString GetInfo(Variant &V);
public:
__fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 27.10. The main source
file for the VarArray program.
///////////////////////////////////////
// Main.cpp
// Project: VarArray
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#pragma link "Grids"
#pragma resource "*.dfm"
#define Max 13
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
AnsiString TForm1::GetInfo(Variant &V)
{
int Count, HighBound, LowBound, i;
AnsiString S;
Count = VarArrayDimCount(V);
S = AnsiString("\nDimension Count: ") + IntToStr(Count) + `\n';
for (i = 1; i <= Count; i++)
{
HighBound = VarArrayHighBound(V, i);
LowBound = VarArrayLowBound(V, i);
S = S + "LowBound: " + IntToStr(LowBound) + `\n';
S = S + "HighBound: " + IntToStr(HighBound) + `\n';
}
return S + `\n';
}
void __fastcall TForm1::bOneDimClick(TObject *Sender)
{
AnsiString S;
int i;
S = "";
Variant MyVariant(OPENARRAY(int, (0, 5)), varInteger);
for (i = 0; i <= 5; i++)
MyVariant.PutElement(i * 2, i);
for (i = 0; i <= 5; i++)
S = S + " " + MyVariant.GetElement(i);
S = GetInfo(MyVariant) + S;
ShowMessage(S);
}
void __fastcall TForm1::bTwoDimClick(TObject *Sender)
{
int i, j;
AnsiString S;
Variant MyVariant(OPENARRAY(int, (0, 5, 0, 5)), varInteger);
for (i = 0; i < 6; i++)
for (j = 0; j < 6; j++)
MyVariant.PutElement(i * j, i, j);
for (i = 0; i < 6; i++)
{
for (j = 0; j < 6; j++)
{
S = S + " " + MyVariant.GetElement(i, j);
}
S = S + `\r';
}
S = GetInfo(MyVariant) + S;
ShowMessage(S);
}
Variant GetArrayData()
{
int i, j;
Variant V(OPENARRAY(int, (1, Max, 1, Max)), varInteger);
for (i = 1; i < Max; i++)
for (j = 1; j < Max; j++)
V.PutElement(i * j, j, i);
return V;
}
void __fastcall TForm1::NormalArray1Click(TObject *Sender)
{
int i, j;
Variant V = GetArrayData();
for (i = 1; i < VarArrayHighBound(V, 1); i++)
for (j = 1; j < VarArrayHighBound(V, 2); j++)
Grid->Cells[i-1][j-1] = V.GetElement(i, j);
}
void __fastcall TForm1::LockedArray1Click(TObject *Sender)
{
int Data[Max][Max];
int i, j;
Variant V;
V = GetArrayData();
void *P = VarArrayLock(V);
memcpy(Data, P, sizeof(Data));
for (i = VarArrayLowBound(V, 1); i < VarArrayHighBound(V, 1); i++)
for (j = VarArrayLowBound(V, 2); j < VarArrayHighBound(V, 2); j++)
Grid->Cells[i-1][j-1] = Data[i-1][j-1];
VarArrayUnlock(V);
}
This program has two menu items:
- One enables you to look at the dimensions and bounds of two different variant
arrays. The first array has one dimension, and the second has two.
- The second pop-up menu enables you to display an array in a string grid using
two different methods. The first method accesses the array through standard techniques,
and the second lets you lock down the data before accessing it.
Remember that variant arrays are of use only in special circumstances. They are
powerful tools, especially when you're making calls to OLE automation objects. However,
they are slower and bulkier than standard BCB arrays and should be used only when
necessary.
Using Remote Datasets with DCOM
The DataCom directory on the CD that accompanies this book contains two
programs. One is an OLE Automation server, and the other is an OLE Automation client.
I will talk about the server first. The code for the server is shown in Listing 27.11
through Listing 27.17. The interface for the server isn't very important from the
perspective of this book, but you can see it in Figure 27.7. Note that the Globals.h
and Globals.cpp files used by both the client and server applications are
stored in the client application's directory.
FIGURE
27.7. The DataServer OLE Automation server
allows you to view the data and test the routines that will be exported to other
applications.
Listing 27.11. The header for
the main module in the DataServer OLE Automation program.
///////////////////////////////////////
// Main.h
// Project: DataServer
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\DBGrids.hpp>
#include "Grids.hpp"
#include <vcl\DBCtrls.hpp>
#include <vcl\ExtCtrls.hpp>
class TForm1 : public TForm
{
__published:
TDBGrid *DBGrid1;
TButton *bFillStrGrid;
TStringGrid *Grid;
TButton *bUpdate;
TDBNavigator *DBNavigator1;
void __fastcall bFillStrGridClick(TObject *Sender);
void __fastcall bUpdateClick(TObject *Sender);
private:
public:
__fastcall TForm1(TComponent* Owner);
Variant __fastcall GetData();
WordBool __fastcall DoUpdate(Variant V);
void __fastcall UpdateParams(AnsiString CustNo, AnsiString Company,
AnsiString Address, AnsiString City, AnsiString State, AnsiString Zip);
};
extern TForm1 *Form1;
#endif
#endif
Listing 27.12. The main source
file for the DataServer application.
///////////////////////////////////////
// Main.cpp
// Project: DataServer
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#include "DMod1.h"
#pragma link "Grids"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
Variant __fastcall TForm1::GetData()
{
TCustomerRecord *Customer = new TCustomerRecord();
void *P;
DMod->GetCustAry(*Customer);
Variant V(OPENARRAY(int, (0, sizeof(TCustomerRecord))), varByte);
P = VarArrayLock(V);
memcpy(P, Customer, sizeof(TCustomerRecord));
VarArrayUnlock(V);
return V;
}
// This function merely tests GetData to make sure it is working
void __fastcall TForm1::bFillStrGridClick(TObject *Sender)
{
TCustomerRecord *Customer = new TCustomerRecord();
void *P;
int i;
Variant V = GetData();
P = VarArrayLock(V);
memcpy(Customer, P, sizeof(TCustomerRecord));
VarArrayUnlock(V);
Grid->RowCount = Customer->Count;
for (i = 0; i < Customer->Count; i++)
{
Grid->Cells[0][i] = Customer->CustAry[i].CustNo;
Grid->Cells[1][i] = Customer->CustAry[i].Company;
Grid->Cells[2][i] = Customer->CustAry[i].Address;
Grid->Cells[3][i] = Customer->CustAry[i].City;
Grid->Cells[4][i] = Customer->CustAry[i].State;
Grid->Cells[5][i] = Customer->CustAry[i].Zip;
}
}
WordBool __fastcall TForm1::DoUpdate(Variant V)
{
void *P;
TCustomer C;
try
{
P = VarArrayLock(V);
memcpy(&C, P, sizeof(TCustomer));
VarArrayUnlock(V);
ShowMessage("Ok");
DMod->Update(C);
}
catch(...)
{
return False;
}
return True;
}
void __fastcall TForm1::UpdateParams(AnsiString CustNo, AnsiString Company,
AnsiString Address, AnsiString City, AnsiString State, AnsiString Zip)
{
TCustomer Customer;
strcpy(Customer.CustNo, CustNo.c_str());
strcpy(Customer.Company, Company.c_str());
strcpy(Customer.Address, Address.c_str());
strcpy(Customer.City, City.c_str());
strcpy(Customer.State, State.c_str());
strcpy(Customer.Zip, Zip.c_str());
DMod->Update(Customer);
}
void __fastcall TForm1::bUpdateClick(TObject *Sender)
{
TCustomer Customer;
AnsiString CustNo;
CustNo = "";
InputQuery("CustNo of Record to Edit", "Enter CustNo: ", CustNo);
strcpy(Customer.Company, "Company");
strcpy(Customer.Address, "Address");
strcpy(Customer.City, "City");
strcpy(Customer.State, "State");
strcpy(Customer.Zip, "Zip");
strcpy(Customer.CustNo, CustNo.c_str());
void *P;
Variant V(OPENARRAY(int, (0, sizeof(TCustomer))), varByte);
P = VarArrayLock(V);
memcpy(P, &Customer, sizeof(TCustomer));
VarArrayUnlock(V);
DoUpdate(V);
}
Listing 27.13. The header for
the data module for the DataServer application.
///////////////////////////////////////
// DMod1.h
// Project: DataServer
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef DMod1H
#define DMod1H
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\DBTables.hpp>
#include <vcl\DB.hpp>
#include "Globals.h"
class TDMod : public TDataModule
{
__published:
TTable *CustomerTable;
TFloatField *CustomerTableCustNo;
TStringField *CustomerTableCompany;
TStringField *CustomerTableAddr1;
TStringField *CustomerTableAddr2;
TStringField *CustomerTableCity;
TStringField *CustomerTableState;
TStringField *CustomerTableZip;
TStringField *CustomerTableCountry;
TStringField *CustomerTablePhone;
TStringField *CustomerTableFAX;
TFloatField *CustomerTableTaxRate;
TStringField *CustomerTableContact;
TDateTimeField *CustomerTableLastInvoiceDate;
TDataSource *CustomerSource;
TQuery *UpdateQuery;
private:
public:
__fastcall TDMod(TComponent* Owner);
void GetCustAry(TCustomerRecord &Customer);
void Update(TCustomer Customer);
};
extern TDMod
*DMod;
#endif
Listing 27.14. The data module
for the DataServer application.
///////////////////////////////////////
// DMod1.cpp
// Project: DataServer
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#include <vcl\bde.hpp>
#pragma hdrstop
#include "DMod1.h"
#pragma resource "*.dfm"
TDMod *DMod;
__fastcall TDMod::TDMod(TComponent* Owner)
: TDataModule(Owner)
{
CustomerTable->Open();
}
void TDMod::GetCustAry(TCustomerRecord &Customer)
{
int i = 0;
Variant V;
Double Num;
CustomerTable->First();
CustomerSource->Enabled = False;
while (!CustomerTable->Eof)
{
Num = CustomerTable->FieldByName("CustNo")->AsFloat;
sprintf(Customer.CustAry[i].CustNo, "%f", Num);
strcpy(Customer.CustAry[i].Company, CustomerTableCompany->AsString.c_str());
strcpy(Customer.CustAry[i].Address, CustomerTableAddr1->AsString.c_str());
strcpy(Customer.CustAry[i].City, CustomerTableCity->AsString.c_str());
strcpy(Customer.CustAry[i].State, CustomerTableState->AsString.c_str());
strcpy(Customer.CustAry[i].Zip, CustomerTableZip->AsString.c_str());
i++;
CustomerTable->Next();
}
Customer.Count = i - 1;
CustomerSource->Enabled = True;
}
void TDMod::Update(TCustomer Customer)
{
float Value;
UpdateQuery->Close();
UpdateQuery->Params->Items[0]->AsString = Customer.Company;
UpdateQuery->Params->Items[1]->AsString = Customer.Address;
UpdateQuery->Params->Items[2]->AsString = Customer.City;
UpdateQuery->Params->Items[3]->AsString = Customer.State;
UpdateQuery->Params->Items[4]->AsString = Customer.Zip;
Value = StrToFloat(Customer.CustNo);
UpdateQuery->Params->Items[5]->AsFloat = Value;
UpdateQuery->ExecSQL();
}
Listing 27.15. The header for
the OLE Automation object in the DataServer application.
///////////////////////////////////////
// DataObject.h
// Project: DataServer
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef DataObjectH
#define DataObjectH
#include <vcl\OleAuto.hpp>
#include <vcl\Classes.hpp>
class TDataServer : public TAutoObject
{
private:
public:
__fastcall TDataServer();
__automated:
AnsiString __fastcall GetName();
Variant __fastcall TDataServer::GetData();
WordBool __fastcall UpdateRecord(Variant V);
void __fastcall UpdateParams(AnsiString CustNo, AnsiString Company,
AnsiString Address, AnsiString City, AnsiString State, AnsiString Zip);
};
#endif
Listing 27.16. The main source
file for the OLE Automation object in the DataServer application.
///////////////////////////////////////
// DataObject.cpp
// Project: DataServer
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "DataObject.h"
#include "DMod1.h"
#include "Main.h"
__fastcall TDataServer::TDataServer()
: TAutoObject()
{
}
AnsiString __fastcall TDataServer::GetName()
{
return AnsiString("TDataServer: ") + Now();
}
Variant __fastcall TDataServer::GetData()
{
return Form1->GetData();
}
WordBool __fastcall TDataServer::UpdateRecord(Variant V)
{
return Form1->DoUpdate(V);
}
void __fastcall TDataServer::UpdateParams(AnsiString CustNo, AnsiString Company,
AnsiString Address, AnsiString City, AnsiString State, AnsiString Zip)
{
Form1->UpdateParams(CustNo, Company, Address, City, State, Zip);
}
void __fastcall RegisterTDataServer()
{
TAutoClassInfo AutoClassInfo;
AutoClassInfo.AutoClass = __classid(TDataServer);
AutoClassInfo.ProgID = "DataServer.DataServer";
AutoClassInfo.ClassID = "{34BADDC0-884F-11D0-BCD7-0080C80CF1D2}";
AutoClassInfo.Description = "DCOM DataServer ";
AutoClassInfo.Instancing = acMultiInstance;
Automation->RegisterClass(AutoClassInfo);
}
#pragma startup RegisterTDataServer
Listing 27.17. The Globals unit
contains some declarations used by both the DataServer and the GetData client applications.
It is stored in the GetData directory.
///////////////////////////////////////
// Globals.h
// Project: GetData
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef GlobalsH
#define GlobalsH
struct TCustomer {
char CustNo[256];
char Company[256];
char Address[256];
char City[256];
char State[256];
char Zip[256];
};
typedef TCustomer TCustAry[100];
struct TCustomerRecord
{
int Count;
TCustAry CustAry;
};
#endif
This program exports the entire Customer table to remote clients. It
also allows the clients to edit a particular row of data. This way, you can both
view and edit the data from a database without ever loading any database tools on
your machine.
NOTE: Understanding that remote datasets
give you many of the advantages of the Web without the slow performance of the Internet
and the limited interface capabilities of HTML is very important. For example, you
can access remote datasets via DCOM without having to load any database tools. All
that you need on your system is the subset of OLE DLLs that concern COM. All these
DLLs, and more, are loaded whenever you launch the Internet Explorer. Both Web browsers
(via ISAPI and CGI) and DCOM give you access to remote datasets. DCOM, however, is
a more efficient, albeit platform-specific, solution.
DCOM is also limited in terms of its range because it works best on an intranet but
might not currently be a viable solution if you're trying to access data halfway
around the world. Once again, the key is to know the available technologies and to
use the one that makes sense in a particular context.
Understanding the ServerData Program
ServerData is fairly long, but the important sections of code are really fairly
brief, and not particularly difficult to understand. I include the whole program
so you can follow the logic of the entire application at your leisure, but I will
focus mostly on a few key elements.
Here is the declaration for the Automation class:
class TDataServer : public TAutoObject
{
private:
public:
__fastcall TDataServer();
__automated:
AnsiString __fastcall GetName();
Variant __fastcall GetData();
WordBool __fastcall UpdateRecord(Variant V);
void __fastcall UpdateParams(AnsiString CustNo, AnsiString Company,
AnsiString Address, AnsiString City, AnsiString State, AnsiString Zip);
};
#endif
The GetName function is provided primarily so that you can test your
connection to the server. If you can call GetName, then you know that you
have access to the server.
The GetData function retrieves a variant array that contains an entire
dataset. At any rate, I grab the key fields from a dataset and iterate through all
the records of the dataset to get the information I need.
The UpdateRecord and UpdateParams functions are used by the
client when it wants to update data on the server. For example, the user might edit
one particular record and then send the edits back to the server via these functions.
NOTE: At the time of this writing, the
first versions of BCB out of the dock apparently will not handle UpdateRecord
properly, although they will handle UpdateParams. The problem with the UpdateRecord
function has to do with what appears to be a bug in how BCB handles variants that
are passed as parameters. In short, you simply cannot pass a variant to a procedure
or function by value; you must pass it by reference. OLE Automation cannot handle
parameters that are passed by reference; you must pass them by value. As a result,
you cannot pass variants as parameters between BCB TAutoObject-based automation
clients and servers. You can, however, return a variant from any BCB method, including
BCB Automation methods.
If you contemplate this bug for a second, you can see that it has absolutely nothing
to do with OLE Automation. That part of the equation is handled fine by BCB and the
VCL. The problem shown here is completely a BCB bug involving its implementation
of variants, and it has nothing to do with the VCL, and nothing to do with OLE Automation.
I'm sure this bug will be fixed very quickly, and you should check to see whether
your version of BCB handles it correctly, or if you can download patches to fix the
problem. I, of course, have not been able to properly test UpdateRecord
because of this bug, but I believe that it will work when the BCB problem is cleaned
up.
The GetData method looks like this:
Variant __fastcall TDataServer::GetData()
{
return Form1->GetData();
}
As you can see, I delegate the actual implementation of GetData to the
main form. This practice is common in OLE Automation because the Automation object
is supposed to be a wrapper around the built-in functionality of your server. For
example, ServerData provides access to the Customer table. The goal of the
Automation server is simply to export that functionality to other programs. As a
result, the fact that the Automation object would simply wrap methods already existing
in the program makes sense.
The TForm1 implementation of GetData looks like this:
Variant __fastcall TForm1::GetData()
{
TCustomerRecord *Customer = new TCustomerRecord();
void *P;
DMod->GetCustAry(*Customer);
Variant V(OPENARRAY(int, (0, sizeof(TCustomerRecord))), varByte);
P = VarArrayLock(V);
memcpy(P, Customer, sizeof(TCustomerRecord));
VarArrayUnlock(V);
return V;
}
This method asks the data module to retrieve a custom structure that contains
the data from the Customer table. I will explain how that process works
in one moment. For now, just concentrate on the fact that the GetData method
converts the custom structure into a variant array by using VarArrayLock
and VarArrayUnlock. This process was described earlier in the chapter, in
the section on the VarArray program.
The custom data structure used by this program consists of an array of TCustomer
structures:
struct TCustomer {
char CustNo[256];
char Company[256];
char Address[256];
char City[256];
char State[256];
char Zip[256];
};
typedef TCustomer TCustAry[100];
The program takes this array and hides inside a custom structure that defines
the number of records in the array:
struct TCustomerRecord
{
int Count;
TCustAry CustAry;
};
Clearly, I could find more memory-efficient ways to store this data, but I wanted
to keep this part of the program simple so that you would be able to follow the logic
of the program without getting bogged down by a mass of irrelevant pointer manipulation.
The important point of this program is how it handles OLE Automation; finding the
best way to store data in memory is really another subject altogether.
After you declare the data structures, you simply need to fill them out in the
data module for the application:
void TDMod::GetCustAry(TCustomerRecord &Customer)
{
int i = 0;
Variant V;
Double Num;
CustomerTable->First();
CustomerSource->Enabled = False;
while (!CustomerTable->Eof)
{
Num = CustomerTable->FieldByName("CustNo")->AsFloat;
sprintf(Customer.CustAry[i].CustNo, "%f", Num);
strcpy(Customer.CustAry[i].Company, CustomerTableCompany->AsString.c_str());
strcpy(Customer.CustAry[i].Address, CustomerTableAddr1->AsString.c_str());
strcpy(Customer.CustAry[i].City, CustomerTableCity->AsString.c_str());
strcpy(Customer.CustAry[i].State, CustomerTableState->AsString.c_str());
strcpy(Customer.CustAry[i].Zip, CustomerTableZip->AsString.c_str());
i++;
CustomerTable->Next();
}
Customer.Count = i - 1;
CustomerSource->Enabled = True;
}
This code simply iterates through the entire dataset, using brute-force methods
to copy the data into the array. Notice that I disable the DataSource for
the module so that the program does not waste time updating the visual display for
a program that is, after all, running on a remote server.
The data module also provides a method for updating the dataset when the user
sends back a record with new data:
void TDMod::Update(TCustomer Customer)
{
float Value;
UpdateQuery->Close();
UpdateQuery->Params->Items[0]->AsString = Customer.Company;
UpdateQuery->Params->Items[1]->AsString = Customer.Address;
UpdateQuery->Params->Items[2]->AsString = Customer.City;
UpdateQuery->Params->Items[3]->AsString = Customer.State;
UpdateQuery->Params->Items[4]->AsString = Customer.Zip;
Value = StrToFloat(Customer.CustNo);
UpdateQuery->Params->Items[5]->AsFloat = Value;
UpdateQuery->ExecSQL();
}
The preceding is just standard TQuery code, of the type that was explained
in depth in Chapter 10, "SQL and the TQuery Object."
The SQL for the UpdateQuery looks like this:
update
Customer
set
Company = :Company,
Addr1 = :Address,
City = :City,
State = :State,
Zip = :Zip
where
CustNo = :CustNo
As you can see, the code will update an existing record given its CustNo.
However, this program makes no provisions for inserting new data. Obviously, adding
that functionality to the program would not be hard, but I have not done so in this
example. In particular, all you would have to do is insert the new record rather
than just update it. You would, however, have to provide a technique for providing
a valid CustNo.
That's all I'm going to say about the ServerData program. You will probably want
to study a few other parts of the program on your own, but overall this program is
not a complex piece of work. One of the great advantages of DCOM and OLE Automation
is that both technologies are easy to use.
GetData: The Client Program for
Remote Datasets
The GetData application, found on the CD that accompanies this book, shows how
to access a remote dataset from a client application. A simple menu allows you to
retrieve a dataset from either a local OLE Automation server or from a remote DCOM
Automation server. In both cases, the server is the ServerData application explained
in the preceding section of this chapter.
After the user connects to the data, it is displayed in the main form of the program,
as shown in Figure 27.8. The user can then edit the data in a custom form, as shown
in Figure 27.9.
FIGURE
27.8. Viewing the data retrieved over
the network from a remote server.
FIGURE
27.9. Editing a row of data before sending
it back to the server.
The code for the GetData program is shown in Listing 27.18 through Listing 27.21.
I do not show the Globals unit here because it was included in the listings
for the ServerData program. I also omit the CodeBox unit, which is found
in the Utils directory on the CD that accom- panies this book. I bring in
the CodeBox unit because I need to call CreateRemoteObject. I supplied
the full source for CreateRemoteObject previously in the section "Creating
the DCOM Client."
Listing 27.18. The header file for
the main module of the GetData application. GetData is an OLE client that retrieves
a database table from a server via OLE Automation.
///////////////////////////////////////
// Main.h
// Project: GetData
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef MainH
#define MainH
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include "Grids.hpp"
#include <vcl\Buttons.hpp>
#include <vcl\Menus.hpp>
#include "globals.h"
class TForm1 : public TForm
{
__published:
TStringGrid *Grid;
TMainMenu *MainMenu1;
TMenuItem *File1;
TMenuItem *MakeConnection1;
TMenuItem *MakeConnectionLocal1;
TMenuItem *N1;
TMenuItem *Exit1;
TMenuItem *Options1;
TMenuItem *Edit1;
TMenuItem *UpdateData1;
void __fastcall MakeConnectionBtnClick(TObject *Sender);
void __fastcall ShowDataClick(TObject *Sender);
void __fastcall Edit1Click(TObject *Sender);
void __fastcall UpdateData1Click(TObject *Sender);
void __fastcall MakeConnection1Click(TObject *Sender);
private:
Variant V;
TCustomerRecord FCustomerRecord;
void FillGrid();
public:
__fastcall TForm1(TComponent* Owner);
};
extern TForm1 *Form1;
#endif
Listing 27.19. The main module
for the GetData program.
///////////////////////////////////////
// Main.cpp
// Project: GetData
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "Main.h"
#include "OleAuto.hpp"
#include "Globals.h"
#include "codebox.h"
#include "EditData1.h"
#include "initguid.h"
#pragma link "Grids"
#pragma resource "*.dfm"
TForm1 *Form1;
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
void __fastcall TForm1::MakeConnectionBtnClick(TObject *Sender)
{
V = CreateOleObject("DataServer.DataServer");
ShowDataClick(NULL);
}
DEFINE_GUID(CLSID_IDATASERVER, 0x34BADDC0, 0x884F, 0x11D0,
0xBC,0xD7,0x00,0x80,0xC8,0x0C,0xF1,0xD2);
void __fastcall TForm1::MakeConnection1Click(TObject *Sender)
{
CreateRemoteObject(CLSID_IDATASERVER, "143.186.149.228", V);
ShowDataClick(NULL);
}
void TForm1::FillGrid()
{
int i;
Grid->RowCount = FCustomerRecord.Count;
for (i = 0; i < FCustomerRecord.Count; i++)
{
Grid->Cells[0][i] = FCustomerRecord.CustAry[i].CustNo;
Grid->Cells[1][i] = FCustomerRecord.CustAry[i].Company;
Grid->Cells[2][i] = FCustomerRecord.CustAry[i].Address;
Grid->Cells[3][i] = FCustomerRecord.CustAry[i].City;
Grid->Cells[4][i] = FCustomerRecord.CustAry[i].State;
Grid->Cells[5][i] = FCustomerRecord.CustAry[i].Zip;
}
}
void __fastcall TForm1::ShowDataClick(TObject *Sender)
{
Variant Data;
void *P;
Data = V.OleFunction("GetData");
P = VarArrayLock(Data);
memcpy(&FCustomerRecord, P, sizeof(TCustomerRecord));
VarArrayUnlock(Data);
FillGrid();
}
void __fastcall TForm1::Edit1Click(TObject *Sender)
{
/* if (EditData->EditCustomer(FCustomerRecord.CustAry[Grid->Selection.Top]) == mrOk)
{
FillGrid();
Variant Temp = EditData->GetCustomerAsVariant();
V.OleFunction("UpDateRecord", Temp);
} */
if (EditData->EditCustomer(FCustomerRecord.CustAry[Grid->Selection.Top]) == mrOk)
{
FillGrid();
EditData->SendCustomerAsStrings(V);
}
}
void __fastcall TForm1::UpdateData1Click(TObject *Sender)
{
ShowDataClick(NULL);
}
Listing 27.20. The EditData module
provides a form for editing an individual record. This is the header file for the
unit.
///////////////////////////////////////// EditData.h
// Project: GetData
// Copyright (c) 1997 by Charlie Calvert
//
#ifndef EditData1H
#define EditData1H
#include <vcl\Classes.hpp>
#include <vcl\Controls.hpp>
#include <vcl\StdCtrls.hpp>
#include <vcl\Forms.hpp>
#include <vcl\ExtCtrls.hpp>
#include <vcl\Buttons.hpp>
#include "globals.h"
class TEditData : public TForm
{
__published:
TLabel *Label1;
TLabel *Label2;
TLabel *Label3;
TLabel *Label4;
TLabel *Label5;
TLabel *Label6;
TBevel *Bevel1;
TEdit *ECompany;
TEdit *EAddress;
TEdit *ECity;
TEdit *EState;
TEdit *EZip;
TEdit *ECustNo;
TBitBtn *BitBtn1;
TBitBtn *BitBtn2;
void __fastcall BitBtn1Click(TObject *Sender);
private:
TCustomer FCustomer;
public:
__fastcall TEditData(TComponent* Owner);
Variant GetCustomerAsVariant();
void GetCustomer();
void FillCustomer();
int EditCustomer(TCustomer &ACustomer);
void SendCustomerAsStrings(Variant &V);
};
extern TEditData *EditData;
#endif
Listing 27.21. The EditData module
provides a form for editing an individual record. You can then send the updated data
back to the server via OLE Automation.
///////////////////////////////////////
// EditData.cpp
// Project: GetData
// Copyright (c) 1997 by Charlie Calvert
//
#include <vcl\vcl.h>
#pragma hdrstop
#include "EditData1.h"
#pragma resource "*.dfm"
TEditData *EditData;
__fastcall TEditData::TEditData(TComponent* Owner)
: TForm(Owner)
{
}
Variant TEditData::GetCustomerAsVariant()
{
void *P;
GetCustomer();
Variant V(OPENARRAY(int, (0, sizeof(TCustomer))), varByte);
P = VarArrayLock(V);
memcpy(P, &FCustomer, sizeof(TCustomer));
VarArrayUnlock(V);
return V;
}
void TEditData::SendCustomerAsStrings(Variant &V)
{
GetCustomer();
V.OleProcedure("UpdateParams", FCustomer.CustNo, FCustomer.Company,FCustomer.Address,
FCustomer.City, FCustomer.State, FCustomer.Zip);
}
void TEditData::GetCustomer()
{
strcpy(FCustomer.CustNo, ECustNo->Text.c_str());
strcpy(FCustomer.Company, ECompany->Text.c_str());
strcpy(FCustomer.Address, EAddress->Text.c_str());
strcpy(FCustomer.City, ECity->Text.c_str());
strcpy(FCustomer.State, EState->Text.c_str());
strcpy(FCustomer.Zip, EZip->Text.c_str());
}
void TEditData::FillCustomer()
{
ECustNo->Text = FCustomer.CustNo;
ECompany->Text = FCustomer.Company;
EAddress->Text = FCustomer.Address;
ECity->Text = FCustomer.City;
EState->Text = FCustomer.State;
EZip->Text = FCustomer.Zip;
}
int TEditData::EditCustomer(TCustomer &ACustomer)
{
FCustomer = ACustomer;
FillCustomer();
int Result = ShowModal();
if (Result == mrOk)
ACustomer = FCustomer;
return Result;
}
void __fastcall TEditData::BitBtn1Click(TObject *Sender)
{
GetCustomer();
}
This program starts out by retrieving the server either locally or remotely:
void __fastcall TForm1::MakeConnectionBtnClick(TObject *Sender)
{
V = CreateOleObject("DataServer.DataServer");
ShowDataClick(NULL);
}
DEFINE_GUID(CLSID_IDATASERVER, 0x34BADDC0, 0x884F, 0x11D0,
0xBC,0xD7,0x00,0x80,0xC8,0x0C,0xF1,0xD2);
void __fastcall TForm1::MakeConnection1Click(TObject *Sender)
{
CreateRemoteObject(CLSID_IDATASERVER, "143.186.149.228", V);
ShowDataClick(NULL);
}
I described all the code shown here in some depth earlier in the chapter. Notice
that I use the GUID from the OLE server to retrieve the program from a remote location.
As I mentioned earlier, you can use the DComCfg.exe application to access
remote servers using the same techniques used with local servers. However, you'll
experience some drawbacks using this system when Windows 95 is involved in the equation.
Figure 27.10 shows DComCfg.exe running on an NT server.
FIGURE
27.10. You can use the DComCfg.exe
utility to make remote servers appear as local servers so that you can call them
with CreateOleObject.
After you connect to the server, you can ask it for a copy of the dataset from
the Customer table and then display the data to the user:
void __fastcall TForm1::ShowDataClick(TObject *Sender)
{
Variant Data;
void *P;
Data = V.OleFunction("GetData");
P = VarArrayLock(Data);
memcpy(&FCustomerRecord, P, sizeof(TCustomerRecord));
VarArrayUnlock(Data);
FillGrid();
}
This code calls the GetData function of the ServerData program. It then
locks down the variant array returned by the server and extracts the custom record
from it. This operation is the reverse of the operation performed in the ServerData,
where you say how to pack the custom data into a variant array.
The FillGrid method simply displays the data in a string grid:
void TForm1::FillGrid()
{
int i;
Grid->RowCount = FCustomerRecord.Count;
for (i = 0; i < FCustomerRecord.Count; i++)
{
Grid->Cells[0][i] = FCustomerRecord.CustAry[i].CustNo;
Grid->Cells[1][i] = FCustomerRecord.CustAry[i].Company;
Grid->Cells[2][i] = FCustomerRecord.CustAry[i].Address;
Grid->Cells[3][i] = FCustomerRecord.CustAry[i].City;
Grid->Cells[4][i] = FCustomerRecord.CustAry[i].State;
Grid->Cells[5][i] = FCustomerRecord.CustAry[i].Zip;
}
}
The Cells property of the TStringGrid object allows you to access
the array of data underlying the grid.
Now that the user can see the remote dataset, the only thing left to do is give
him or her a chance to edit it. The following line of code retrieves the currently
selected row from the string grid:
if (EditData->EditCustomer(FCustomerRecord.CustAry[Grid->Selection.Top])
== mrOk)
The key point to grasp here is that Grid->Selection.Top designates
the currently selected row in the grid.
Inside the TEditData form, only one routine is of any real interest.
This routine is called GetCustomerAsVariant:
Variant TEditData::GetCustomerAsVariant()
{
void *P;
GetCustomer();
Variant V(OPENARRAY(int, (0, sizeof(TCustomer))), varByte);
P = VarArrayLock(V);
memcpy(P, &FCustomer, sizeof(TCustomer));
VarArrayUnlock(V);
return V;
}
This code uses the GetCustomer function, which follows, to retrieve the
newly edited data from the TEditData form. It then moves the data into a
variant array by first locking down the array and then moving some bytes around via
a call to memcpy. Here is the simple GetCustomer method used to
retrieve the data from the visual controls in the TEditForm:
void TEditData::GetCustomer()
{
strcpy(FCustomer.CustNo, ECustNo->Text.c_str());
strcpy(FCustomer.Company, ECompany->Text.c_str());
strcpy(FCustomer.Address, EAddress->Text.c_str());
strcpy(FCustomer.City, ECity->Text.c_str());
strcpy(FCustomer.State, EState->Text.c_str());
strcpy(FCustomer.Zip, EZip->Text.c_str());
}
If you don't want to pass the data back to the server using a variant array, you
can just pass the strings of the record back directly:
void TEditData::SendCustomerAsStrings(Variant &V)
{
GetCustomer();
V.OleProcedure("UpdateParams", FCustomer.CustNo, FCustomer.Company,
FCustomer.Address, FCustomer.City, FCustomer.State, FCustomer.Zip);
}
This code retrieves the text that the user has edited and then calls the UpdateParams
procedure of the OLE server. UpdataParams will execute a SQL update statement
to insert the new data into the Customer table.
That's all I'm going to say about remote datasets. They're one of the more powerful
aspects of DCOM, and I'm sure you can imagine many other ways to use this technology.
If you want to extend this technology with a set of robust tools, you should look
into Entera, a remote client/server technology provided by Borland.
Summary
In this chapter, you learned how to use BCB to build applications that take advantage
of the Distributed Component Object Model, or DCOM. You have seen that combining
BCB, DCOM, and OLE Automation provides a simple method for allowing one application
to control or use another application that resides on a second machine.
People who are interested in this field should look at Borland's Entera and OLEnterprise
products, as well as the very powerful OLE-based tools found in Delphi 3.0. The plan
at the time of this writing is for all the tools in Delphi 3.0 to appear in future
versions of BCB.
Windows programmers have seen so many extraordinary technical developments in
the last few years that it's difficult to single out any one technology and say that
it is significantly more important than the rest. Nevertheless, DCOM appears to be
a viable solution to one of the major problems faced by contemporary programmers.
In short, we can now easily distribute the workload of a particular product across
multiple machines. Just how much impact this technology will have on the industry
is hard to say at this early stage, but DCOM (and related technologies such as CORBA)
certainly have the potential to change the way we build applications.
|